package com.zf.websocket.endpoint;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 定义端点的方式一：使用 @ServerEndpoint 定义端点，通过 ServerEndpointExporter 类注册WebSocket端点。
 * <p>
 * 定义一个 WebSocket 服务端点, 可以定义多个端点，相当于controller
 * <p>
 * 1、项目启动时会创建一个 HelloWebSocketServer对象，并加入容器，这个对象的中的属性会被注入。
 * 2、以后每次建立WebSocket连接后，都会创建一个新的 HelloWebSocketServer对象，之后在该连接对应的 HelloWebSocketServer 对象 中执行内部的 open、close、error、message 方法，
 * 不会再经过spring，所以这些对象的属性无法注入，
 * 3、所以只能定义静态属性，在spring启动时对静态属性进行注入，以后所有的新建的HelloWebSocketServer对象共享静态属性。
 * <p>
 * 所有的对象属性都是线程安全的
 */
@Slf4j
@Component
// @ ServerEndpoint 功能主要是将目前的类定义成一个websocket端点, 注解的值表示服务端点的 URI 地址, 客户端可以通过这个URI来访问WebSocket服务
// 指定该端点的地址，这里面的地址：可以使用路径参数、在此处使用通配符是无效的（/hello/* 就匹配 /hello/*，不会匹配其他）。
@ServerEndpoint("/hello")
public class HelloEndpoint {
    /**
     * concurrent包的线程安全Set，用来存放每个客户端对应的 HelloWebSocketServer对象。
     */
    private static final ConcurrentHashMap<String, HelloEndpoint> webSocketMap = new ConcurrentHashMap<>();
    /**
     * 与某个客户端的连接会话，需要通过它来给客户端发送数据
     * 每次建立连接都会创建新的 HelloWebSocketServer对象，把session对象 设置给新建的 HelloWebSocketServer对象，所以不用担心并发问题
     */
    private Session session;

    /**
     * 连接建立成功后调用的方法，@OnOpen 注解的方法会在建立连接成功的时候调用，名称可以自定义，@OnError、@OnMessage、@OnClose 同理
     * <p>
     * 每次建立连接都会产生一个新的 HelloWebSocketServer 对象，之后在该连接对应的 HelloWebSocketServer 对象 中执行其 open、close、error、message 方法。
     * 每次建立连接后都会产生一个 session 对象。
     *
     * @param session 会话
     * @param config 当前端点的配置信息，不需要则可以不写该参数
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) throws IOException {
        log.info("[客户端] {} 连接成功", session.getId());
        this.session = session;
        webSocketMap.put(session.getId(), this);
        sendMessage("建立连接成功！");

    }

    /**
     * 连接关闭调用的方法，此时已经断开了连接
     *
     * @param closeReason 连接关闭的原因，写不写该参数都可以，不用则可以不写
     */
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        log.info("[客户端] {} 断开连接：{}", session.getId(), closeReason);
        System.out.println(session.isOpen());
        webSocketMap.remove(session.getId());
    }

    /**
     * 收到客户端普通文本消息后调用的方法
     *
     * @param text 客户端发送过来的文本消息
     */
    @OnMessage
    public void onTextMessage(String text, Session session) throws IOException {
        log.info("[客户端] {} :{}", session.getId(), text);
        sendMessage(String.format("客户 %s 您好，来自server的响应", session.getId()));
    }


    /**
     * 收到客户端的二进制消息后调用的方法，如果不需要处理二进制数据则可以不用写该方法
     *
     * @param byteBuffer 客户端发送过来的二进制消息
     */
    @OnMessage
    public void onBinaryMessage(ByteBuffer byteBuffer, Session session) throws IOException {
        log.info("[客户端] {} :{}", session.getId(), byteBuffer);
        // 返回给发送端
        session.getBasicRemote().sendBinary(byteBuffer);
    }


    /**
     * 发生错误时执行的方法
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误:" + ",原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        // 同步，阻塞直到消息发出，再向下执行
        this.session.getBasicRemote().sendText(message);
        // 异步发送，立即返回，再向下执行
        //this.session.getAsyncRemote().sendText(message);
    }
}
